<template>
  <AModalPro v-bind="$attrs" title="文件上传" :maskClosable="false" :footer="null" @cancel="onCancel">
    <AUploadDragger
      class="upload-container"
      :showUploadList="false"
      name="file"
      :multiple="false"
      :beforeUpload="beforeUpload"
    >
      <div class="upload-body">
        <div class="upload-body__icon">
          <InboxOutlined></InboxOutlined>
        </div>
        <div class="upload-body__text">单击或拖动文件到此区域进行上传</div>
      </div>
    </AUploadDragger>

    <div class="r-upload-list-container">
      <div class="upload-item" v-for="item in fileList" :key="item.id">
        <div class="upload-item-info">
          <div class="upload-item-info__name">{{ item.file?.name }}</div>
          <div class="upload-item-info__status">{{ item.status }}</div>

          <div class="upload-item-info__progress" v-show="item.status === statusEnum.uploading">
            {{ item.progress }} %
          </div>
          <div class="upload-item-info__close" @click="() => removeFile(item.file)">
            <CloseOutlined />
          </div>
        </div>
        <div class="upload-item__progress">
          <div class="upload-item__progress-load" :style="{ width: item.percent + '%' }"></div>
          <div class="upload-item__progress-upload" :style="{ width: item.progress + '%' }"></div>
        </div>
      </div>
    </div>
  </AModalPro>
</template>

<script setup>
import { toValue } from 'vue';
import { AModalPro } from '@/components/iteration';
import { InboxOutlined, CloseOutlined } from '@ant-design/icons-vue';
import {
  fetchRecoverDeleteFile,
  fetchGetFileUploadCheck,
  fetchFileUploadSlice,
  fetchFileSliceMerge,
  fetchFileSliceVerify
} from '@/api/file';
import { createChunks, threadComputeHashByIdle } from '@/utils/file';
import { useState } from '@/hooks';

defineOptions({
  inheritAttrs: false // 禁用透传
});

const emit = defineEmits(['cancel', 'finish']);

const [fileList, setFileList] = useState([]);
const [uploadCount, setUploadCount] = useState(0);
const maxUploadCount = 3;

const statusEnum = {
  awaiting: '等待中',
  paused: '已暂停',
  loading: '资源加载',
  checking: '文件校验中',
  uploading: '正在上传',
  merging: '资源整合中',
  success: '上传成功',
  error: '上传失败'
};

const onCancel = () => {
  emit('cancel');
  if (toValue(fileList).length) {
    emit('finish');
  }
};

const beforeUpload = (file) => {
  // 获取文件信息
  setFileList((v) => [...v, { file, key: file.uid, status: statusEnum.awaiting }]);
  toValue(uploadCount) <= maxUploadCount && handleUpload(file);
  return false;
};

const onUploadFile = (file, state) => {
  const data = [...toValue(fileList)];
  const index = data.findIndex((v) => v.file.uid === file.uid);
  data[index] = { ...data[index], file, ...state };
  setFileList(data);
};

const removeFile = (file) => {
  const data = [...toValue(fileList)];
  const index = data.findIndex((v) => v.file.uid === file.uid);
  data.splice(index, 1);
  setFileList(data);
};

const handleAssert = async (file) => {
  return new Promise((resolve, reject) => {
    const index = [...toValue(fileList)].findIndex((v) => v.file.uid === file.uid);
    return index === -1 ? reject('中断') : resolve();
  });
};

/**
 * 文件分片上传
 *
 * @param file
 * @param {Array<Blob>} data 文件分片数组
 * @param {Function} onProgress 进度回调
 */
const sendUploadFileSlice = (file, data, onProgress) => {
  return new Promise((resolve) => {
    let progress = 0;
    const nextRequest = async (index = 0) => {
      await handleAssert(file);
      await fetchFileUploadSlice(data[index]);
      progress += data[index].chunkFile.size;
      onProgress?.(progress);
      return ++index >= data.length ? resolve() : nextRequest(index);
    };

    nextRequest();
  });
};

/**
 * 更新文件校验进度
 *
 * @param {File} file 文件对象
 * @param {Number} current 当前进度（分片大小）
 * @param {Number} total 总进度
 */
const updateLoadProgress = async (file, current, total) => {
  await handleAssert(file);
  const progress = current / total;
  const percent = Math.floor(progress * 100);
  onUploadFile(file, { percent });
};

/**
 * 更新文件上传进度
 *
 * @param {File} file 文件对象
 * @param {Number} current 当前进度（分片大小）
 * @param {Number} total 总进度
 */
const updateUploadProgress = (file, current, total) => {
  const progress = current / total;
  const percent = Math.floor(progress * 100);
  onUploadFile(file, { progress: percent });
};

/**
 * 恢复文件删除数据
 *
 * @param {File} file 文件对象
 * @param {string} fileHash 文件哈希
 */
const recoverUpload = async (file, fileHash) => {
  await fetchRecoverDeleteFile(fileHash).catch(() => {
    onUploadFile(file, { status: statusEnum.error });
  });
  onUploadFile(file, { progress: 100, status: statusEnum.success });
};

/**
 * 文件上传
 *
 * @param {File} file 文件对象
 */
const handleUpload = async (file) => {
  await handleAssert(file);
  setUploadCount((v) => v + 1);
  onUploadFile(file, { status: statusEnum.checking });
  const fileHash = await threadComputeHashByIdle(file, (...v) => updateLoadProgress(file, ...v));

  await handleAssert(file);
  let { breakPointIndex = -1, uploadedSize = 0, isExist = false } = await fetchGetFileUploadCheck(fileHash);

  await handleAssert(file);
  // 更新上传进度
  updateUploadProgress(file, uploadedSize, file.size);

  if (isExist) {
    return recoverUpload(file, fileHash);
  }

  onUploadFile(file, { status: statusEnum.uploading });

  // 创建文件分片
  const chunks = await createChunks(file, uploadedSize);
  chunks.forEach((chunk, index) => {
    chunk.fileHash = fileHash;
    chunk.index = index + breakPointIndex + 1;
  });

  await handleAssert(file);
  // 上传文件分片
  await sendUploadFileSlice(file, chunks, (v) => updateUploadProgress(file, v + uploadedSize, file.size));
  await handleAssert(file);
  handleSliceMerge(file, fileHash);
};

/**
 * 文件分片合并
 *
 * @param {File} file 文件对象
 * @param {string} fileHash 文件哈希
 * @returns void
 */
const handleSliceMerge = async (file, fileHash) => {
  const merge = {
    fileHash,
    fileSize: file.size,
    mineType: file.type,
    fileName: file.name.slice(0, file.name.lastIndexOf('.')),
    fileSuffix: file.name.slice(file.name.lastIndexOf('.') + 1)
  };

  onUploadFile(file, { status: statusEnum.merging });

  const taskId = await fetchFileSliceMerge(merge);
  // 文件分片合并校验
  const timer = setInterval(async () => {
    await handleAssert(file).catch(() => clearInterval(timer));
    const res = await fetchFileSliceVerify(taskId).catch(() => {
      onFinish(file, 'FAIL');
      clearInterval(timer);
    });
    if (res !== 'AWAIT') {
      onFinish(file, res);
      clearInterval(timer);
    }
  }, 2000);
};

const onFinish = (file, res) => {
  const status = res === 'SUCCESS' ? statusEnum.success : statusEnum.error;
  onUploadFile(file, { status });
  setUploadCount((v) => v - 1);
  // 下一个文件上传
  const nextFile = toValue(fileList).find((v) => v.status === statusEnum.awaiting);
  nextFile && handleUpload(nextFile);
};
</script>

<style lang="scss">
.ant-upload-wrapper.upload-container {
  display: block;
  padding: 0 24px;
  margin-bottom: 24px;

  .upload-body {
    padding: 24px 0;

    &__icon .action {
      color: #1677ff;
      font-size: 48px;
    }

    &__text {
      margin-top: 16px;
    }
  }
}

.r-upload-list-container {
  width: 100%;
  display: flex;
  flex-direction: column;
  padding: 0 16px;
  gap: 8px;
  max-height: 240px;
  overflow: auto;

  .upload-item {
    width: 100%;
    display: flex;
    flex-direction: column;
    position: relative;
    cursor: pointer;
    border-radius: 4px;

    &:hover {
      background-color: #f5f7fa;
    }

    &-info {
      width: 100%;
      display: flex;
      align-items: center;
      gap: 8px;
      position: relative;

      &__name {
        flex-grow: 1;
        padding: 0 8px;
      }

      &__status {
        font-size: 14px;
      }

      &__close {
        color: #909399;
        padding: 4px 8px;
        &:hover {
          color: #000;
        }
      }
    }

    &__progress {
      display: flex;
      width: 100%;
      height: 4px;
      gap: 8px;
      background-color: #ebeef5;
      position: relative;

      &-load {
        left: 0;
        top: 0;
        height: 100%;
        border-radius: 2px;
        position: absolute;
        background-color: #909399;
        transition: width 0.4s linear;
      }

      &-upload {
        left: 0;
        top: 0;
        height: 100%;
        border-radius: 2px;
        position: absolute;
        background-color: #1677ff;
        transition: width 0.4s linear;
      }
    }
  }
}
</style>
